package service

import (
	"encoding/json"
	"fmt"
	"github.com/beego/beego/v2/server/web/context"
	"github.com/google/uuid"
	pkgerrors "github.com/pkg/errors"
	"net/http"
	"scase.io/application-auto-scaling-service/pkg/api/errors"
	"scase.io/application-auto-scaling-service/pkg/api/model"
	"scase.io/application-auto-scaling-service/pkg/api/model/cci"
	"scase.io/application-auto-scaling-service/pkg/cloudresource"
	"scase.io/application-auto-scaling-service/pkg/common"
	"scase.io/application-auto-scaling-service/pkg/db"
	"scase.io/application-auto-scaling-service/pkg/service/taskservice"
	"scase.io/application-auto-scaling-service/pkg/setting"
	"scase.io/application-auto-scaling-service/pkg/utils/logger"
	"time"
)

type PodService struct {
	Ctx     *context.Context
	tLogger *logger.FMLogger
}

func NewCreatePodGroupService(ctx *context.Context, tLogger *logger.FMLogger) *PodService {
	s := &PodService{
		Ctx:     ctx,
		tLogger: tLogger,
	}
	return s
}

func (s *PodService) CreatePodGroup(oriProjectId string, req cci.CreatePodRequest) (*model.CreateScalingGroupResp, *errors.ErrorResp) {
	if err := db.AddOrUpdateAgencyInfo(getPodAgencyInfo(req, oriProjectId)); err != nil {
		s.tLogger.Error("Add or update AgencyInfo to db err: %+v", err)
		return nil, errors.NewErrorRespWithMessage(errors.ServerInternalError, err.Error())
	}
	// 1. add to db
	podGroup, err := s.addScalingGroup(oriProjectId, req)
	if err != nil {
		s.tLogger.Error("Add scaling group info err: %+v", err)
		return nil, errors.NewErrorRespWithMessage(errors.ServerInternalError, err.Error())
	}
	s.tLogger.Info("create ecs group success, group id:%s", podGroup.Id)
	// 2. async task
	taskErr := taskservice.StartEcsScalingOutTask(podGroup, req.InstancePodGroup.DesireInstanceNumber)
	if taskErr != nil {
		if !pkgerrors.Is(taskErr, common.ErrNoNeedToDo) {
			s.tLogger.Error("Start pod scaling out task error:%+v", taskErr)
			return nil, errors.NewErrorRespWithMessage(errors.ServerInternalError, taskErr.Error())
		}
	}
	resp := &model.CreateScalingGroupResp{
		ScalingGroupId: podGroup.Id,
	}
	return resp, nil
}

// GetInstancePodGroup get instance Pod group detail
func GetInstancePodGroup(log *logger.FMLogger, projectId, groupId string) (*model.ScalingGroupDetail, *errors.ErrorResp) {
	if exist := db.IsScalingGroupExist(projectId, groupId); !exist {
		log.Error("The pod group[%s] of project[%s] is not found", groupId, projectId)
		return nil, errors.NewErrorRespWithHttpCode(errors.PodGroupNotFound, http.StatusNotFound)
	}
	group, err := db.GetScalingGroupById(projectId, groupId)
	if err != nil {
		log.Error("Read pod group[%s] from db err: %+v", groupId, err)
		return nil, errors.NewErrorRespWithHttpCode(errors.ServerInternalError, http.StatusNotFound)
	}
	detail := convertDaoScalingGroup(group)
	return &detail, nil
}

func (s *PodService) addScalingGroup(oriProjectId string, req cci.CreatePodRequest) (*db.ScalingGroup, error) {
	ic, err := getNewInstanceConfiguration(*req.InstancePodGroup.InstanceConfiguration, uuid.NewString())
	if err != nil {
		s.tLogger.Error("getNewInstanceConfiguration error:%s", err.Error())
		return nil, err
	}
	if err = db.AddInstanceConfiguration(ic); err != nil {
		s.tLogger.Error("AddInstanceConfiguration error:%s", err.Error())
		return nil, err
	}
	group, err := convertPodRequestToDB(req, oriProjectId, ic)
	if err != nil {
		s.tLogger.Error("convertPodRequestToDB error:%s", err.Error())
		return nil, err
	}
	if err = db.ScalingGroupTable().AddNewScalingGroup(group); err != nil {
		return nil, err
	}

	s.tLogger.Info("insert scaling pod group success, pod group:%+v", group)
	return group, nil
}

func convertPodRequestToDB(req cci.CreatePodRequest, oriProjectId string, config *db.InstanceConfiguration) (*db.ScalingGroup, error) {
	setEnv(&req)
	pt, err := json.Marshal(req.InstancePodGroup.PodTemplate)
	if err != nil {
		return nil, err
	}
	coolDownTime := common.DefaultCoolDownTime
	if req.InstancePodGroup.CoolDownTime != nil {
		coolDownTime = *req.InstancePodGroup.CoolDownTime
	}

	scalingGroup := &db.ScalingGroup{
		InstanceConfiguration: config,
		Id:                    uuid.NewString(),
		FleetId:               req.InstancePodGroup.FleetId,
		Name:                  req.InstancePodGroup.FleetId,
		InstanceType:          req.InstancePodGroup.InstanceType,
		ResourceId:            uuid.NewString(),
		MinInstanceNumber:     req.InstancePodGroup.MinInstanceNumber,
		MaxInstanceNumber:     req.InstancePodGroup.MaxInstanceNumber,
		DesireInstanceNumber:  req.InstancePodGroup.DesireInstanceNumber,
		CoolDownTime:          coolDownTime,
		SubnetId:              req.InstancePodGroup.SubnetId,
		VpcId:                 req.InstancePodGroup.VpcId,
		PodTemplate:           string(pt),
		EnableAutoScaling:     req.InstancePodGroup.EnableAutoScaling,
		EnterpriseProjectId:   req.InstancePodGroup.EnterpriseProjectId,
		ProjectId:             oriProjectId,
	}
	return scalingGroup, nil
}

// appgw 调用
func (s *PodService) GetPodInfo(log *logger.FMLogger, hostName string) (*cci.QueryPodInfoResp, *errors.ErrorResp) {
	if !db.IsPodExit(hostName) {
		log.Error("get podInfo by HostName[%s] fail, data is not exist.", hostName)
		return nil, errors.NewErrorRespWithHttpCode(errors.PodInfoNotFound, http.StatusInternalServerError)
	}
	podInfo, err := db.GetPodInfoByHostName(hostName)
	if err != nil {
		log.Error("get podInfo by hostname[%s] from db err: %+v", hostName, err)
		return nil, errors.NewErrorRespWithMessage(errors.PodInfoNotFound, err.Error())
	}
	if podInfo.State != db.PodStateActive {
		podInfo.State = db.PodStateActive
		podInfo.ActiveTime = time.Now().UTC()
		if err := db.PodInfoTable().Update(podInfo); err != nil {
			log.Error("update pod info error:%+v", err)
			return nil, errors.NewErrorRespWithMessage(errors.DbError, err.Error())
		}
	}
	resp := buildQueryPodInfoResp(podInfo)
	return resp, nil
}

func (s *PodService) QueryPod(log *logger.FMLogger, oriProjectId, namespace, podName string) (*cci.QueryPodResp, error) {
	podCtrl, err := cloudresource.GetPodController(oriProjectId)
	if err != nil {
		log.Error("get pod controller error:%+v", err)
		return nil, err
	}
	queryResp, err := podCtrl.QueryPod(namespace, podName)
	if err != nil {
		log.Error("query  namespace [%s] pod [%s] error:%+v", namespace, podName, err)
		return nil, err
	}
	return queryResp, nil
}

func (s *PodService) UpdatePodGroup(projectId string, group *db.ScalingGroup, req model.UpdateScalingGroupReq) error {
	// 从db读出来之后的状态A，在此函数结尾写入db时，决策扩缩容时的状态B会被A覆盖，因此记录修改的col
	cols := []string{}
	if err := s.UpdateScalingGroupConfig(req, group, &cols); err != nil {
		s.tLogger.Error("UpdateScalingGroupTagsAndInstanceConfiguration error:%+v", err)
		return err
	}
	// 修正desNum，扩缩容决策
	if err := s.UpdateInstanceNumberParam(req, group, &cols); err != nil {
		s.tLogger.Error("update instance number param error:%+v", err)
		return err
	}
	if err := s.JudgeScalingNum(group, group.DesireInstanceNumber, &cols); err != nil {
		s.tLogger.Error("judge scaling number error:%+v", err)
		return err
	}

	// 将group信息update到数据库，指定col是为了防止修改state时的并发冲突
	if err := db.UpdateScalingGroup(group, cols...); err != nil {
		s.tLogger.Error("Update scaling group[%s] to db err: %+v", group.Id, err)
		return err
	}
	return nil
}

// 决策扩容，缩容
// 考虑伸缩组正在扩容中，ecs还未上报，此时再决策会造成重复扩容，因此扩容的数量应该要考虑异步任务中正在扩容的个数
// 正在扩容时，伸缩组还未稳定，不会缩容，因此只要在扩容时考虑
func (s *PodService) JudgeScalingNum(group *db.ScalingGroup, targetNum int32, cols *[]string) error {
	s.tLogger.Info("group [%s] current instance num:%d, desire num:%d, new target num:%d ",
		group.Id, group.CurrentInstanceNumber, group.DesireInstanceNumber, targetNum)

	if group.CurrentInstanceNumber > targetNum {
		s.tLogger.Info("group [%s] current num:%d > target num:%d, will scaling in",
			group.Id, group.CurrentInstanceNumber, targetNum)
		// 启动缩容任务
		if err := taskservice.StartManualPodScaleInTask(group, group.CurrentInstanceNumber-targetNum); err != nil {
			s.tLogger.Error("start scale in task error:%s", err.Error())
			return err
		}
		return nil
	}
	// 扩容可以复用
	if group.CurrentInstanceNumber < targetNum {
		if err := taskservice.StartEcsScalingOutTask(group, targetNum); err != nil {
			if !pkgerrors.Is(err, common.ErrNoNeedToDo) {
				s.tLogger.Error("start scaling out task error:%+v", err)
				return err
			}
		}
		return nil
	}
	return nil
}

// group通过指针引用接收修改
func (s *PodService) UpdateScalingGroupConfig(req model.UpdateScalingGroupReq, group *db.ScalingGroup, cols *[]string) error {
	// 更新instanceConfig
	if req.InstanceConfiguration != nil {
		instanceConfig, err := updateInstanceConfiguration(*req.InstanceConfiguration, group.InstanceConfiguration)
		if err != nil {
			s.tLogger.Error("Update instance configuration err: %+v", err)
			return err
		}
		if err := db.UpdateInstanceConfiguration(instanceConfig); err != nil {
			s.tLogger.Error("Update instance config error:%+v", err)
			return err
		}
	}
	// 开启、关闭自动伸缩，允许自动伸缩需要先绑定策略
	if req.EnableAutoScaling != nil {
		exist := db.GroupPolicyRelationTable().CheckRelationExist(group.Id, "")
		if !exist && *req.EnableAutoScaling {
			s.tLogger.Error(errors.AutoScalingUpdateError.Msg())
			return fmt.Errorf("auto scaling need at least one scaling policy")
		}
		*cols = append(*cols, "enable_auto_scaling")
		group.EnableAutoScaling = *req.EnableAutoScaling
	}
	if req.CoolDownTime != nil {
		group.CoolDownTime = int64(*req.CoolDownTime)
		*cols = append(*cols, "cool_down_time")
	}
	return nil
}

func (s *PodService) UpdateInstanceNumberParam(req model.UpdateScalingGroupReq, group *db.ScalingGroup, cols *[]string) error {
	originDes := group.DesireInstanceNumber
	if req.MaxInstanceNumber != nil {
		group.MaxInstanceNumber = *req.MaxInstanceNumber
		if group.DesireInstanceNumber > *req.MaxInstanceNumber {
			group.DesireInstanceNumber = *req.MaxInstanceNumber
		}
		*cols = append(*cols, "max_instance_number")
	}
	if req.MinInstanceNumber != nil {
		group.MinInstanceNumber = *req.MinInstanceNumber
		if group.DesireInstanceNumber < *req.MinInstanceNumber {
			group.DesireInstanceNumber = *req.MinInstanceNumber
		}
		*cols = append(*cols, "min_instance_number")
	}
	if req.DesireInstanceNumber != nil {
		group.DesireInstanceNumber = *req.DesireInstanceNumber
		*cols = append(*cols, "desire_instance_number")
	}
	if group.MaxInstanceNumber < group.MinInstanceNumber || group.DesireInstanceNumber < group.MinInstanceNumber ||
		group.DesireInstanceNumber > group.MaxInstanceNumber {
		return fmt.Errorf("invalid instance number, do not match minimun:%d <= desire:%d <= maximun:%d",
			group.MinInstanceNumber, group.DesireInstanceNumber, group.MaxInstanceNumber)
	}
	if originDes != group.DesireInstanceNumber {
		*cols = append(*cols, "desire_instance_number")
	}
	return nil
}

func buildQueryPodInfoResp(podInfo *db.PodInfo) *cci.QueryPodInfoResp {
	return &cci.QueryPodInfoResp{
		HostName:       podInfo.HostName,
		ProjectID:      podInfo.ProjectId,
		FleetId:        podInfo.FleetId,
		Namespace:      podInfo.Namespace,
		ScalingGroupID: podInfo.ScalingGroupId,
		InstanceID:     podInfo.InstanceId,
		PublicIP:       podInfo.PublicIp,
		PrivateIP:      podInfo.PrivateIp,
		HealthState:    podInfo.State,
		CreateTime:     podInfo.CreateAt.Format(common.TimeLayout),
	}
}

func (s *PodService) DeletePodGroup(log *logger.FMLogger, projectId, groupId string) error {
	if !db.IsScalingGroupExist(projectId, groupId) {
		log.Info("The scaling group[%s] of project[%s] is not found", groupId, projectId)
		return nil
	}
	if err := taskservice.StartDeletePodScaleGroupTask(groupId); err != nil {
		// 任务已存在，直接返回
		if pkgerrors.Is(err, common.ErrDeleteTaskAlreadyExists) {
			log.Info("Delete task for group[%s] is running, do nothing", groupId)
			return nil
		}
		log.Error("Start delete scaling group task for group[%s] err: %+v", groupId, err)
		return err
	}

	return nil
}

func setEnv(req *cci.CreatePodRequest) {
	podEnvs := []cci.PodSpecContainersEnv{}
	podEnvs = append(podEnvs, cci.PodSpecContainersEnv{
		Name:  common.GateWayAddressKey,
		Value: setting.GetAppGwEndpoint(),
	})
	podEnvs = append(podEnvs, cci.PodSpecContainersEnv{
		Name:  common.EnvInstanceType,
		Value: common.InstanceTypePod,
	})
	for index, _ := range req.InstancePodGroup.PodTemplate.Spec.Containers {
		req.InstancePodGroup.PodTemplate.Spec.Containers[index].Env = podEnvs
	}
}
